# Copyright (c) 2009 Peter Corbett
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

import collections
import random

import levelgen
import utils
import time

class GameState:
    def __init__(self, data, pc_class):
        self.id_maker = utils.IDMaker()
        
        self.messages = collections.deque([], 50)
        self.is_alive = True
        self.is_victorious = False
        self.cheat_omniscent = False

        self.lostab = None
        
        self.pc = Mob(self.id_maker.new_id(),0,0,pc_class,data,None,True)

        self.levels = [None for i in range(10)]
        self.level = 0
        self.levels[0] = Level(self, data, self.level)
        self.current_level = self.levels[0]
        self.pc.x, self.pc.y = self.current_level.stairs_up[0]
        #self.pc.inv.append(Item(0))
        #self.pc.inv.append(Item(1))
        #self.pc.inv.append(Item(2))
        self.pc.sortinv(data)
        #for i in range(len(data.spelltypes)):
        #    self.pc.spells.append(i)


        self.running = None
        self.resting = None

        self.damage_indicator = None

        self.xp = 0
    
    def is_playing(self):
        return self.is_alive and not self.is_victorious


class Level:
    def __init__(self, state, data, depth):
        self.state = state
        self.depth = depth
        self.floortiles = [[0 for j in range(20)] for i in range(80)]
        self.known = [[False for j in range(20)] for i in range(80)]
        self.mobs = []
        self.items = [[list() for j in range(20)] for i in range(80)]
        self.stairs_up = []
        self.stairs_down = []
        levelgen.generate(self, state, data, depth)

    def get_mob_place(self, data):
        placeable = False
        while not placeable:
            x = random.randint(1,78) 
            y = random.randint(1,18) 
            placeable = self.passable(x, y, data, None, True)
        return (x, y)

    def passable(self, x, y, data, pc=None, placeable=False):
        if x < 0 or y < 0 or x >= 80 or y >= 20:
            return False
        if pc != None and pc.x == x and pc.y == y:
            return pc
        for mob in self.mobs:
            if mob.x == x and mob.y == y:
                return mob
        ftd = data.floortiles[self.floortiles[x][y]]
        return ftd.placemob if placeable else ftd.passable

    def add_item(self, x, y, item):
        s = self.items[x][y]
        if len(s) > 0 and s[-1].can_merge_with(item):
            s[-1].number += item.number
            del item
        else:
            s.append(item)

    def add_items(self, x, y, items):
        for i in items:
            self.add_item(x, y, i)

    def learn_los(self, lostab):
        for x in range(80):
            for y in range(20):
                if lostab[x][y]:
                    self.known[x][y] = True
                    
class Mob:
    def __init__(self, id, x, y, type, data, itemsel=None, is_pc=False):
        self.id = id
        self.is_pc = is_pc
        self.x = x
        self.y = y
        self.type = type
        self.time = 0
        self.initiative = 0
        self.inv = []
        self.slots = {}
        self.spells = []

        self.statuses = {}
        self.aidict = {}
        
        mtype = data.mobtypes[type]

        self.inv.extend(data.generate_items(mtype.startitems))
        for item in list(self.inv):
            self.wield_item(item, data)
        
        for spell in mtype.spells:
            self.spells.append(data.spelldict[spell])

        self.str, self.agi, self.int, self.wp = [i + random.randint(-1, 1) + random.randint(-1, 1) for i in mtype.stats]
        self.level = mtype.level

        self.hp = -1 # calc_stats will fix this
        self.mp = -1
        self.hpr = 0
        self.mpr = 0

        self.intrinsic = data.basedict.copy()
        utils.add_dict(self.intrinsic, mtype.intrinsic)

        self.calc_stats(data)

        if random.random() < mtype.auchance:
            self.au = random.randint(1, mtype.au) + random.randint(1, mtype.au)
        else:
            self.au = 0

        if itemsel:
            for chance in mtype.ichances:
                if random.random() < chance:
                    itype = itemsel.select()
                    self.inv.append(Item(itype))

    def desc(self, data, article, initcap):
        if self.is_pc:
            s = "you"
        else:
            s = "%s %s" % (article, data.mobtypes[self.type].name)
        if initcap:
            s = utils.firstupper(s)
        return s

    def can_see(self, other, state, data, ignore_stealth=False):
        if self == other:
            return True
        if not self.is_pc and not other.is_pc:
            return True
        in_los = state.lostab[self.x][self.y] if other.is_pc else state.lostab[other.x][other.y]
        if "blind" in self.statuses:
            return False
        if not ignore_stealth and "stealth" in other.statuses:
            return False
        return in_los

    def adjacent_to(self, other):
        if isinstance(other, Mob):
            return abs(self.x - other.x) <= 1 and abs(self.y - other.y) <= 1
        else:
            return abs(self.x - other[0]) <= 1 and abs(self.y - other[1]) <= 1

    def range_to(self, other):
        "Octagonal range: 3x major, 2x minor"
        dx = abs(self.x - other.x)
        dy = abs(self.y - other.y)
        return 3 * max(dx, dy) + 2 * min(dx, dy)

    def move_to(self, location, state, data):
        if self is state.pc:
            for m in state.current_level.mobs:
                if (not m.adjacent_to(self)) and m.adjacent_to(location):
                    m.initiative_check(state.pc, data)
        else:
            if (not self.adjacent_to(state.pc) and state.pc.adjacent_to(location)):
                self.initiative_check(state.pc, data)
        self.x, self.y = location

    def initiative_check(self, pc, data):
        if self.initiative == 0:
            if "afraid" in pc.statuses or "afraid" in self.statuses:
                return
            can_see_pc = (not "blind" in self.statuses) and (not "stealth" in pc.statuses)
            pc_can_see = (not "blind" in pc.statuses) and (not "stealth" in self.statuses)
            if can_see_pc and not pc_can_see:
                self.initiative = 1
                return
            if pc_can_see and not can_see_pc:
                self.initiative = -1
                return
            if (not pc_can_see) and not can_see_pc:
                self.initiative = 0
            pcir = random.randint(0, max(1, pc.sdict["init"]))
            mir = random.randint(0, max(1, self.sdict["init"]))
            self.initiative = cmp(mir,pcir)

    def spot_check(self, other, state, data, pgprl):
        #if self is state.pc:
        #    if not state.lostab[other.x][other.y]:
        #        return
        #else:
        #    if not state.lostab[self.x][self.y]:
        #        return
        if "stealth" in other.statuses:
            if not self.can_see(other, state, data, ignore_stealth=True):
                return
            dist = self.range_to(other)
            spot = self.sdict["spot"]
            sneak = other.sdict["sneak"] + dist
            if random.randint(0, max(1, spot)) > random.randint(0, max(1, sneak)):
                v = "spot" if self.is_pc else "spots"
                pgprl.add_message("%s %s %s!" % (self.desc(data, "the", True), v, self.desc(data, "the", False)))
                other.del_status("stealth", data)

    def calc_stats(self, data):
        self.sdict = self.intrinsic.copy()
        for slot in self.slots:
            utils.add_dict(self.sdict, data.itemtypes[self.slots[slot].type].dict)
        for status in self.statuses:
            utils.add_dict(self.sdict, self.statuses[status].dict)

        self.sdict["enc"] = max(0, self.sdict["enc"] - self.str)
        enc = self.sdict["enc"]

        estr = ((self.str) * (self.level + 10)) / 10
        eagi = ((self.agi) * (self.level + 10)) / 10
        eint = ((self.int) * (self.level + 10)) / 10
        ewp  = ((self.wp) * (self.level + 10)) / 10

        hw = self.sdict["hthweight"]
        rhw = min(hw, self.str)
        xshw = 0 if rhw == hw else hw - rhw
        xsstr = 0 if rhw == self.str else self.str - rhw

        self.sdict["speed"] -= enc

        self.sdict["hthspeed"] -= xshw
        self.sdict["hthhit"] += max(0, eagi - xshw)
        self.sdict["hthpow"] += 2 * rhw + xshw + xsstr + (estr - self.str)
        self.sdict["hthdef"] += max(0, eagi - xshw) # Parry
        self.sdict["hthdef"] += max(0, eagi - enc)  # Dodge
        
        self.sdict["mishit"] += eagi
        self.sdict["mispow"] += eagi / 2
        self.sdict["misdef"] += max(0, eagi - enc) / 2
        
        self.sdict["magspeed"] += ((eint + eagi) / 2) - 10
        self.sdict["maghit"] += eint
        self.sdict["magpow"] += eint + ewp
        self.sdict["magdef"] += ewp

        self.sdict["init"] += max(0, eagi - enc) + max(0, eagi - xshw)
        self.sdict["spot"] += eagi + 2 * eint
        self.sdict["sneak"] += 2 * max(0, eagi - enc) + eint

        self.sdict["courage"] += ewp
        
        self.maxhp = (estr + ewp) * self.level
        if self.hp == -1:
            self.hp = self.maxhp
        else:
            self.hp = min(self.hp, self.maxhp)
        self.maxmp = (eint + ewp) * self.level
        if self.mp == -1:
            self.mp = self.maxmp
        else:
            self.mp = min(self.mp, self.maxmp)

        self.movtime = utils.scale_speed(self.sdict["speed"] - enc)
        self.atktime = utils.scale_speed(self.sdict["hthspeed"])
        self.matktime = utils.scale_speed(self.sdict["misspeed"])
        self.acttime = utils.scale_speed(0)
        self.casttime = utils.scale_speed(self.sdict["magspeed"])

    def add_status(self, status, duration, data):
        sn = data.statusdict[status]
        st = data.statustypes[sn]
        s = Status(sn, duration, self, dict(st.dict))
        for mx in st.mutex:
            if mx in self.statuses:
                del self.statuses[mx]
        self.statuses[status] = s
        self.calc_stats(data)

    def del_status(self, status, data):
        if status in self.statuses:
            del self.statuses[status]
        self.calc_stats(data)

    def wield_item(self, item, data):
        itemtype = data.itemtypes[item.type]
        slot = itemtype.slots
        if slot == None or slot not in data.slotdict:
            return False
        else:
            if slot in self.slots:
                self.slots[slot].slot = None
            if item.number > 1 and itemtype.slots not in ("arrow", "slingstone", "thrown"):
                item = item.split(1)
                self.inv.append(item)
            item.slot = slot
            self.slots[slot] = item
            for ex in data.slottypes[data.slotdict[slot]].mutex:
                if ex in self.slots:
                    self.slots[ex].slot = None
                    del self.slots[ex]
            self.sortinv(data)
            return True
    
    def sortinv(self, data):
        self.inv.sort(key = lambda i:
                 (i.slot, data.itemtypes[i.type].slots, i.type),
                      reverse = True)
                    
        i = 0
        while i < len(self.inv) - 1:
            pslot = data.itemtypes[self.inv[i+1].type].slots
            if self.inv[i].can_merge_with(self.inv[i+1]):
                self.inv[i].number += self.inv[i+1].number
                del self.inv[i+1]
            elif (pslot
                  and self.inv[i+1].slot == None
                  and pslot in ("thrown", "slingstone", "arrow")
                  and pslot in self.slots
                  and self.slots[pslot].can_merge_with_ammo(self.inv[i+1])):
                self.slots[pslot].number += self.inv[i+1].number
                del self.inv[i+1]
            else:
                i += 1

    def regen(self, time, data):
        recalc = False
        for status in self.statuses.keys():
            if self.statuses[status].tick(time):
                recalc = True
                del self.statuses[status]
        if recalc:
            self.calc_stats(data)
        self.hpr += time * self.maxhp
        self.hp = max(0, min(self.maxhp, self.hp + self.hpr / 10000))
        self.hpr %= 10000
        if self.hp == self.maxhp:
            self.hpr = 0
        self.mpr += time * self.maxmp
        self.mp = max(0, min(self.maxmp, self.mp + self.mpr / 10000))
        self.mpr %= 10000
        if self.mp == self.maxmp:
            self.mpr = 0

    def moralecheck(self, damage, data):
        df = damage / (1.0 * self.maxhp)
        hpf = self.hp / (1.0 * self.maxhp)
        wpf = self.sdict["courage"] / 20.0
        dff = 1 if random.random() < df else 0
        hpff = 1 if random.random() >= hpf else 0
        wpff = 1 if random.random() >= wpf else 0
        if dff + hpff + wpff >= 3:
            self.add_status("fear", 100, data)
            return True
        return False

class Item:
    def __init__(self, type, number=1):
        self.type = type
        self.slot = None
        self.number = number

    def desc(self, data, article, oneItem=False):
        itype = data.itemtypes[self.type]
        if oneItem or self.number == 1:
            if article:
                return "%s %s" % (article, itype.name)
            else:
                return itype.name
        else:
            if itype.name.startswith("pair of "):
                plural = "pairs of " + itype.name[8:]
            else:
                plural = itype.name + "s"
            return "%s %s" % (self.number, plural)

    def can_merge_with(self, other):
        return self.type == other.type and self.slot == None and other.slot == None

    def can_merge_with_ammo(self, other):
        return self.type == other.type and other.slot == None

    def split(self, num):
        self.number -= num
        return Item(self.type, num)

class Status:
    def __init__(self, type, duration, mob, sdict):
        self.type = type
        self.duration = duration
        self.mob = mob
        self.dict = sdict

    def tick(self, time):
        if self.duration is None:
            return False
        dt = min(time, self.duration)
        if "mpregen" in self.dict:
            self.mob.mpr += dt * self.dict["mpregen"]
            if self.dict["mpregen"] < 0 and self.mob.mp + (self.mob.mpr / 10000) <= 0:
                return True
        if "hpregen" in self.dict:
            self.mob.hpr += dt * self.dict["hpregen"]
        self.duration -= time
        if self.duration <= 0:
            return True
        return False
